﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Text;

namespace Infrastructure.CrossCutting.Caching
{
    /// <summary>
    /// When a cache item is removed from one of the load balanced servers,
    /// all other servers should be notified.
    /// This cache archive this goal by taking advantage of a distributed cache.
    /// </summary>
    /// <remarks>
    /// Suggest relatively longer expiry time for this cache.
    /// Otherwise, the local caches will keep refreshing.
    /// The expiry for local caches is averagely half of the time.
    /// </remarks>
    public class CompositeCache : ICache
    {
        #region Static

        private static long CreateNewToken()
        {
            // Primitive types are more efficient than GUID when processing by memcached.
            // We don't need the precision here, but Stopwatch is more efficient than DateTime.UtcNow.
            // There can be conflicts of the token generated on different web servers,
            //   but it happens only when the cache is set almost at the same time from both servers,
            //   which should be fine and acceptable.
            return System.Diagnostics.Stopwatch.GetTimestamp();
        }

        #endregion Static

        #region Fields

        public readonly IDistributedCache _distributedCache;
        public readonly ILocalCache _localCache;
        public static readonly TimeSpan NoSlidingExpiration = TimeSpan.Zero;
        #endregion Fields

        #region Constructor

        public CompositeCache(IDistributedCache distributedCache, ILocalCache localCache)
        {
            _distributedCache = distributedCache;
            _localCache = localCache;
        }

        #endregion Constructor

        #region ICache Members

        public object Get(CacheKey cacheKey)
        {
            var item = _localCache.Get<CompositeCacheItem>(cacheKey);
            if (item == null) return null; // not in local cache
            if (bool.Parse(ConfigurationManager.AppSettings["Memcached"]) == true)
            {
                var token = _distributedCache.Get<long>(cacheKey);
                if (item.Token != token)
                {
                    // different from distributed cache. invalid local cache.
                    _localCache.Remove(cacheKey);
                    return null;
                }
            }

            return item.Value;
        }

        public void Set(CacheItemConfig cacheItemConfig, object value)
        {
            var token = _distributedCache.Get<long>(cacheItemConfig.CacheKey);
            if (token == default(long))
            {
                token = CreateNewToken();
                // only set a new token when the token is not exists in distributed cache
                // Jan 31, 2012, KLiu: no expiry on distributed cache.
                if (bool.Parse(ConfigurationManager.AppSettings["Memcached"]) == true)
                {
                    _distributedCache.Set(new CacheItemConfig(cacheItemConfig.CacheKey, NoSlidingExpiration), token);
                }
            }

            var item = new CompositeCacheItem { Token = token, Value = value };
            _localCache.Set(cacheItemConfig, item);
        }

        public bool Remove(CacheKey cacheKey)
        {
            _localCache.Remove(cacheKey);
            return _distributedCache.Remove(cacheKey); // the cache was valid only if it was valid on distributed cache
        }

        public void Clear()
        {
            _distributedCache.Clear();
            _localCache.Clear();
        }

        #endregion ICache Members
    }
}
